package com.googlecode.skald.client;

import java.util.Collections;
import java.util.EnumSet;
import java.util.List;
import java.util.logging.Logger;

import com.google.gwt.core.client.Scheduler;
import com.google.gwt.dom.client.AnchorElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.NodeList;
import com.google.gwt.dom.client.Style;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.logging.client.HasWidgetsLogHandler;
import com.google.gwt.user.client.Command;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.Anchor;
import com.google.gwt.user.client.ui.DockLayoutPanel;
import com.google.gwt.user.client.ui.HTMLPanel;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.MenuBar;
import com.google.gwt.user.client.ui.MenuItem;
import com.google.gwt.user.client.ui.ScrollPanel;
import com.google.gwt.user.client.ui.SplitLayoutPanel;
import com.google.gwt.user.client.ui.TabLayoutPanel;
import com.google.gwt.user.client.ui.VerticalPanel;
import com.googlecode.skald.client.affordances.Affordance;
import com.googlecode.skald.client.affordances.Affordances;
import com.googlecode.skald.client.affordances.MenuOption;
import com.googlecode.skald.client.cmds.DisplayedSendCmd;
import com.googlecode.skald.client.ui.GameObject;
import com.googlecode.skald.client.ui.GameObjectRegistry;
import com.googlecode.skald.client.ui.HidingPopup;

/**
 * The main top-level root GUI "window" that should be added to the GWT 
 * root panel. It also has references to all major subcomponents.
 * 
 * @author Zach Tomaszewski
 */
public class GUI extends DockLayoutPanel {
  
  public final static String USER_INPUT_PREFIX = "> ";
  
  //private static final Logger logger = Logger.getLogger(GUI.class.getName());
  private final static int ACTIONS_TAB = 0;
  private final static int OBJECTS_TAB = 1;
  
  /** 
   * By default, the game's affordance-based game controls are given
   * in a couple menus.  Set this to true to instead move these menus
   * to tabs instead. 
   */
  protected boolean gameControlsAsPanel = true;
  
  /** The main game content window */
  protected VerticalPanel main; 
  /** The scroll panel that contains the main content */
  protected ScrollPanel mainScroller;
  
  /** Top-level menu bar */
  protected MenuBar menu;
  protected MenuItem skaldMenu;  
  protected MenuItem gameMenu;  
  protected MenuItem actionsMenu;
  protected MenuItem objectsMenu;
  protected MenuItem helpMenu;  
  
  /** 
   * Tabs to be used in place of menu bar, depending on 
   * {@link #gameControlsAsPanel}.
   */
  protected TabLayoutPanel gameControlTabs;
  
  /**
   * The root split layout that (currently) holds the gameControlTabs to 
   * left and main content to right.
   */
  protected SplitLayoutPanel slp;
  
  /** Alert location for 1-line messages to user */
  protected Label statusBar;
  
  /** Where to log to */
  protected VerticalPanel logPanel;
  protected HidingPopup logPopup;
  
  /** State set by last response received from the server */
  private List<Affordance> affordances;
  private HTMLPanel lastResponse = null;
  private HTMLPanel lastInput = null;
  private GameObjectRegistry gameObjectRegistry;
  
  
  /** 
   * Constructs a new top-level GUI panel
   */
  public GUI() {
    super(Style.Unit.EM);
    
    //construct hidden components
    this.logPanel = new VerticalPanel();
    Logger.getLogger("").addHandler(new HasWidgetsLogHandler(this.logPanel));
    this.logPopup = new HidingPopup(this.logPanel, "20em", true);

    //construct upper menu bar
    this.menu = new MenuBar();

    // skald menu
    this.skaldMenu = new MenuItem("Skald", new MenuBar(true));

    // * Game Controls toggle
    final MenuItem tabToggle = new MenuItem(this.getGameControlToggleText(),
                                            (Command) null);
    tabToggle.setScheduledCommand(new Scheduler.ScheduledCommand() {
          @Override
          public void execute() {
            GUI.this.gameControlsAsPanel = !GUI.this.gameControlsAsPanel;
            GUI.this.refreshGameControls();
            tabToggle.setText(GUI.this.getGameControlToggleText());
          }      
        });
    this.skaldMenu.getSubMenu().addItem(tabToggle);
    
    // * Show Logging
    this.skaldMenu.getSubMenu().addItem(new MenuItem("Show Logging",
        new Command() {
          @Override
          public void execute() {
            logPopup.show();           
          }      
    }));
    this.menu.addItem(this.skaldMenu);
  
    // game-specific menu
    /*
    this.gameMenu = new MenuItem("Game", new MenuBar(true));
    this.menu.addItem(this.gameMenu);
    */
  
    // actions and objects currently possible in game
    this.actionsMenu = new MenuItem("Actions", new MenuBar(true));
    this.objectsMenu = new MenuItem("Objects", new MenuBar(true));
    this.actionsMenu.addStyleName("skald-gameMenu-header");
    this.objectsMenu.addStyleName("skald-gameMenu-header");
    // added later to menu, if necessary, by refreshGameControls()
    
    // help menu
    this.helpMenu = new MenuItem("Help", new MenuBar(true));
    helpMenu.getSubMenu().addItem("About Skald", new Command() {
      public void execute() {
        Window.alert("Skald - " +
        		"an affordance-based web UI for interactive fiction\n" +
        		"by Zach Tomaszewski\n\n" +
        		"http://skald.googlecode.com");
      }
    });
    this.menu.addItem(this.helpMenu);
    this.addNorth(this.menu, 1.8);
    
    // add status bar
    this.statusBar = new Label("Welcome to Skald.");
    this.statusBar.addStyleName("skald-statusBar");
    this.addSouth(this.statusBar, 1.5);
    
    // finally, the main window (needs to be added last to go in center)
    this.main = new VerticalPanel();
    this.mainScroller = new ScrollPanel(this.main);
    this.refreshGameControls();
    
    // GUI state structures
    this.gameObjectRegistry = new GameObjectRegistry(
                                EnumSet.of(MenuOption.COLLAPSE_HORIZONTALLY,
                                           MenuOption.COLLAPSE_VERTICALLY),
                                new DisplayedSendCmd(null));
  }
  
  private String getGameControlToggleText() {
    return "Show Game Controls as " + 
        (this.gameControlsAsPanel ? "Menus" : "Tabs");
  }

  /** 
   * Creates game control tabs (or removes them) according to 
   * {@link #gameControlsAsPanel}
   */
  public void refreshGameControls() {
    if (this.gameControlsAsPanel) {
      if (this.slp == null) {
        // lazy init
        this.slp = new SplitLayoutPanel();
        this.gameControlTabs = new TabLayoutPanel(1.5, Unit.EM);
        this.gameControlTabs.add(this.actionsMenu.getSubMenu(), "Actions");
        this.gameControlTabs.add(this.objectsMenu.getSubMenu(), "Objects");
        slp.addLineStart(this.gameControlTabs, 140); //pixel width
      }
      if (this.slp.getParent() != this) {
        //need to show it
        if (this.mainScroller.getParent() == this) {
          this.remove(this.mainScroller);
        }
        slp.add(this.mainScroller);
        this.add(slp);
        
        // remove menus
        this.menu.removeItem(this.actionsMenu);
        this.menu.removeItem(this.objectsMenu);
      }      
    }else {
      if (this.mainScroller.getParent() != this) {
        if (this.slp != null && this.slp.getParent() == this){
          this.remove(this.slp);
          this.slp.remove(this.mainScroller);
        }
        this.add(this.mainScroller); // to center so it expands
        
        // add menus
        this.menu.insertItem(this.actionsMenu, 1);
        this.menu.insertItem(this.objectsMenu, 2);
      }
    }
  }
  
  /**
   * Clears the status bar of any messages.
   */
  public void clearStatus() {
    this.statusBar.setText(" ");
    this.statusBar.removeStyleName("skald-statusBar-error");
  }
  
  /**
   * Sets the status bar to display the given mesg text.
   */
  public void setStatus(String mesg) {
    this.clearStatus();
    this.statusBar.setText(mesg);
  }

  /**
   * Sets the status bar to display the given error mesg text.
   * As an error, this is prefaced by "Error: " and appended with
   * " (See Skald -> Logging for details.)"
   */
  public void setStatusError(String mesg) {
    this.statusBar.setText("Error: " + mesg + 
        " (See Skald -> Show Logging for details.)");
    this.statusBar.addStyleName("skald-statusBar-error");
  }

  /**
   * Shows the given message as user input.  This will also disable the 
   * last response, if any.  Scrolls to the bottom of the window.
   */
  public void displayInput(String userInput) {
    if (lastInput != null) {
      lastInput.removeStyleName("skald-input");
      lastInput.addStyleName("skald-input-disabled");
    }
    lastInput = new HTMLPanel(USER_INPUT_PREFIX + userInput);
    lastInput.addStyleName("skald-input");
    if (this.lastResponse != null) {
      this.disableResponse(this.lastResponse);
      this.lastResponse = null;
    }
    this.main.add(lastInput);
    this.mainScroller.scrollToBottom();
  }
  
  /**
   * Displays the given server response.  Will activate all object anchors
   * before displaying.  Displaying a new response like this will inactivate
   * the currently-active response.
   */
  public void displayResponse(String html) {
    //disable currently active response
    HTMLPanel response = new HTMLPanel(html);
    response.addStyleName("skald-response");
    if (this.lastResponse != null) {
      disableResponse(this.lastResponse);
    }

    //process new to activate links
    this.enableResponse(response);

    //display and record
    this.main.add(response);
    this.lastResponse = response;
    this.mainScroller.ensureVisible(response);
    
//TEST: empty response
  }

  /**
   * Greys out the given HTML, disable all of its object anchor links, 
   * and removes the links from the current links map.
   */
  protected void disableResponse(HTMLPanel response) {
    response.removeStyleName("skald-response");
    response.addStyleName("skald-response-disabled");
    this.gameObjectRegistry.clear();
  }

  /**
   * Converts all object links to Anchors with the corresponding object's 
   * affordance menu.
   *   
   * @param response  A HTML response sent from server 
   */
  protected void enableResponse(HTMLPanel response) {
    NodeList<Element> anchors = response.getElement().getElementsByTagName("A");
    for (int i = 0; i < anchors.getLength(); i++) {
      AnchorElement ancElem = AnchorElement.as(anchors.getItem(i));
      String objName = GameObject.getObjectName(ancElem.getHref());
      if (objName != null) {
        // convert to Anchor object view of raw underlying A element
        Anchor ancWidget = new Anchor(ancElem.getInnerHTML());
        response.addAndReplaceElement(ancWidget, ancElem);        
        gameObjectRegistry.add(objName, ancWidget); 
      }
    }
//TEST: object with no corresponding affordances given    
  }

  /** Returns an unmodifiable copy of the GUI's current affordances list.*/
  public List<Affordance> getAffordances() {
    return Collections.unmodifiableList(this.affordances);    
  }

  /**
   * Establishes all affordances currently supported by the game state and
   * that should thus be shown through the GUI.  
   * <p>
   * Setting this has the side-effect of immediately rebuilding the Actions 
   * menu for all possible actions. The stored affordances list will also 
   * be used to produce all object-specific menus.
   * 
   * @param affordances
   */
  public void setAffordances(List<Affordance> affordances) {
    this.affordances = affordances;
    this.actionsMenu.setSubMenu(Affordances.getMenu(affordances));
    if (this.slp != null) {
      // also refresh contents of (possibly hidden) tabs
      int selected = this.gameControlTabs.getSelectedIndex();
      this.gameControlTabs.remove(ACTIONS_TAB);
      this.gameControlTabs.insert(this.actionsMenu.getSubMenu(), 
                                  "Actions", ACTIONS_TAB);
      if (selected == ACTIONS_TAB) {
        this.gameControlTabs.selectTab(selected);
      }
    }
  } 

  /**
   * Establishes the list of currently clearly-afforded objects.
   * This rebuilds the objects menu, where each object leads to a 
   * sub-menu of afforded actions for the object.  There may be no such
   * submenus of there are no affordances for that object.  
   * <p>
   * Uses the current affordances set (so {@link #setAffordances(List)} 
   * should usually be called before this method), but does not affect 
   * the game object registry.
   * 
   * @param objects  A list of object names
   */
  public void setObjects(List<String> objects) {
    MenuBar bar = new MenuBar(true);
    for (String obj : objects) {
      List<Affordance> affs = Affordances.forObject(this.affordances, obj);
      MenuBar sub = null;
      if (affs.size() > 0) {
        sub = Affordances.getMenu(affs, 
                EnumSet.of(MenuOption.COLLAPSE_HORIZONTALLY,
                           MenuOption.COLLAPSE_VERTICALLY));
      }
      bar.addItem(obj, sub); //may still be null 
    }
    this.objectsMenu.setSubMenu(bar);
    if (this.slp != null) {
      // refresh contents of (possibly hidden) tabs.  Preserve current select.
      int selected = this.gameControlTabs.getSelectedIndex();
      this.gameControlTabs.remove(OBJECTS_TAB);
      this.gameControlTabs.insert(this.objectsMenu.getSubMenu(), 
                                  "Objects", OBJECTS_TAB);
      if (selected == OBJECTS_TAB) {
        this.gameControlTabs.selectTab(selected);
      }
    }
    //FIXME: Switching b/w tabs and menus is still flaky: one or other will
    // disappear until refreshed
  } 
}
